iT邦幫忙

2024 iThome 鐵人賽

DAY 26
0

Day25 我們談到了列舉類型 (enum),某種程度來講,列舉類型也算是一種條件限制,它限制了填入特定欄位的值只能是某個固定的集合。

條件限制在 SQL 資料庫來講,最常用的條件限制不外乎下列三種:

  1. 不可為空限制 (not null)
  2. 不可重複限制 (unique)
  3. 外鍵 (foreign key)

就我個人的經驗,在簡單的使用案例,使用上述三種已足以確保相當程度的系統正確性。接下來,我們來對 Datomic 在常見應用的對應作法做討論。

常見應用

主鍵

在 SQL 來講,當設定主鍵 (primary) 給特定欄位時,也就等同於設定了兩個條件限制:不容許空值 (NULL)、且值不可重複。

然而,在 Datomic 不需思考這個問題,因為主鍵就是資料實體編碼 (entity id),資料實體編碼在整個資料庫之內都不會重複使用、且任何的資料實體都一定會有一個編碼,所以必然滿足上述的兩個性質。

不可為空

Datomic 並沒有這種條件限制的對應語法,因為 Datomic 不容許你對任何的屬性寫入空值 NULL,所以也不需要這種語法。

那該怎麼表現某個資料實體沒有某個屬性呢?

很簡單,不要寫入資料庫即可。比方說,像下方的 交易資料 (tx-data),我們寫入了兩個 item,每個 item 都有兩個屬性。

[{:db/id item-1-id
  :line-item/product chocolate
  :line-item/quantity 1}
 {:db/id item-2-id
  :line-item/product whisky
  :line-item/quantity 2}]

如果說,我們要讓 item-3 沒有 :line-item/quantity 呢?寫成下方的形式即可:

[{:db/id item-3-id
  :line-item/product happy}]

讀者注意到了嗎?這就是欄位綱要 (column schema) 的優勢,它帶來了更清晰的語意。

不可重複

參考下方的範例,在 SQL 的不可重複條件限制,在 Datomic 只要在對應的屬性加上
:db/unique :db.unique/identity 即可。

{:db/ident :user/uuid,
 :db/valueType :db.type/uuid,
 :db/doc "Unique user identifier",
 :db/cardinality :db.cardinality/one,
 :db/unique :db.unique/identity}

外鍵

Datomic 沒有提供簡潔的語法來讓使用者寫出外鍵的語意,

外鍵固然有確保資料一致性好處,但是它也會造成了額外的開發成本,特別是在做測試的時候。很多時候,我想做個簡單的整合測試,需要在資料庫裡放入一些測試資料,本來想說,只要生成一張資料表的測試資料即可。結果,因為有了外鍵,結果變成我得生成三張資料表的測試資料。

也因此,我傾向認為,Datomic 是刻意不對外鍵條件限制設計簡潔的語法,因為它不鼓勵一定要這麼做。

進階應用

在常見應用的外鍵,我們提到了 Datomic 沒有提供簡潔的外鍵寫法。前面這句話只說了一半,實際上,一旦使用了 Datomic 的自訂斷言函數,當然也是沒有什麼複雜的條件限制表達不出來的。

在之前 Day18 時,我們有示範,如果有一些比較複雜的聚合查詢難以表達,我們可以用自訂聚合函數,而且這還可以讓語意更加清晰。Datomic 在條件限制也是一樣的設計,它提供兩種自訂的條件限制,一種是屬性斷言函數 (Attribute Predicates),可用來強化屬性的語意;另一種是資料實體規格 (Entity Specs),可用來對資料實體本身做出種種限制。

這邊又要再做一個詞彙的釐清,在 Clojure/Datomic 的語境裡,條件限制 (constraints) 與規格 (spec) 幾乎是同義詞,常常會交替使用。

屬性斷言函數

有時候,我們指定某個屬性的資料型態是字串,但是,實際上,我們存入該屬性的永遠是 Email,而 Email 有固定的格式。在這種情況下,我們就可以用屬性斷言函數來進一步強化語意。

下方,我們用「檢查姓名的字串的長度必須大於等於 3 且小於等於 15 個字元」來示範屬性斷言函數。

  • 先定義自訂函數 'datomic.samples.attr-preds/user-name?
ns datomic.samples.attr-preds)

(defn user-name?
  [s]
  (<= 3 (count s) 15))
  • 將定義的自訂函數指定給 :db.attr/preds ,就完成屬性斷言函數的設定。
{:db/ident :user/name,
 :db/valueType :db.type/string,
 :db/cardinality :db.cardinality/one,
 :db.attr/preds 'datomic.samples.attr-preds/user-name?}

資料實體規格

屬性斷言函數可以強化單一屬性的限制語意,那如果我們想要限制的特性,它是屬於整個資料實體的呢?比方說,特定的資料實體,至少要包含某些特定的屬性,又或是資料彼此之間的互相關系。

這種隸屬於資料實體的條件限制,Datomic 提供了資料實體規格 (Entity Specs) 的語法來我們表達。

首先,資料實體規格本身也是一種資料實體。讀者從一路讀過來,已經讀過了很多 Datomic 用資料實體來表現的語意了。這邊做一個小整理,本系列文中用資料實體表現過的資料包含:

  • 商業領域的資料,比方說:人、商品等
  • 交易
  • 屬性
  • 列舉類型
  • 資料實體規格 (Entity Specs)

必須的欄位

先看一個例子:利用資料實體規格來限制「必須欄位」,作法有兩個步驟:

  • 定義資料實體規格

下方,我們定義一個叫做 :user/validate 的資料實體規格,它規定必須包含 :user/name:user/email 兩個欄位。

{:db/ident        :user/validate
 :db.entity/attrs [:user/name :user/email]}
  • 利用在交易資料裡的 :db/ensure 觸發檢查

下方是一筆交易資料,注意到,我們額外加上了一個 :db/ensure 這個虛擬屬性,它會讓 Datomic 在寫入資料時,特別檢查此時此刻寫入的資料實體是否滿足對應的資料實體規格

{:user/name "John Doe"
 :db/ensure :user/validate}

資料之間的關系

資料之間的關系就必須要在資料實體規格裡也呼叫函數,即資料實體斷言函數 (Entity Predicates)。下方看一個例子,它由三個步驟構成:

  • 先定義自訂函數 datomic.samples.entity-preds/scores-are-ordered?
(ns datomic.samples.entity-preds
  (:require [datomic.api :as d]))

(defn scores-are-ordered?
  [db eid]
  (let [m (d/pull db [:score/low :score/high] eid)]
    (<= (:score/low m) (:score/high m))))
  • 將定義的自訂函數指定給 :db.entity/preds ,就完成資料實體斷言函數的設定。
{:db/ident        :score/guard
 :db.entity/attrs [:score/low :score/high] ;; required attributes
 :db.entity/preds 'datomic.samples.entity-preds/scores-are-ordered?} ;; entity predicate
  • 利用在交易資料裡的 :db/ensure 觸發檢查

下方的 :db/ensure 會讓這筆交易資料在寫入時,觸發 :score/guard 來做檢查。

{:score/low 100
 :score/high 20
 :db/ensure :score/guard}

參考資料

其它資源

  1. 歡迎訂閱 PruningSuccess 電子報,主要談論軟體開發、資料處理、資料分析等議題。
  2. 歡迎加入 Clojure 社群

上一篇
主鍵 (primary key) 與資料實體編碼 (entity id)
下一篇
索引與效能
系列文
Datomic,內建事件溯源的資料庫。30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言